﻿// Copyright (c) .NET Foundation and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Roslynator.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Roslynator.CSharp.CSharpFactory;

namespace Roslynator.CSharp.Refactorings;

internal static class IntroduceConstructorRefactoring
{
    public static async Task ComputeRefactoringsAsync(RefactoringContext context, MemberDeclarationSyntax declaration)
    {
        if (!context.IsRefactoringEnabled(RefactoringDescriptors.IntroduceConstructor))
            return;

        List<MemberDeclarationSyntax> members = null;

        SyntaxKind kind = declaration.Kind();

        if (kind.Is(SyntaxKind.PropertyDeclaration, SyntaxKind.FieldDeclaration))
        {
            if (context.Span.Contains(declaration.Span))
            {
                SemanticModel semanticModel = await context.GetSemanticModelAsync().ConfigureAwait(false);

                if (CanBeAssignedFromConstructor(declaration, context.Span, semanticModel, context.CancellationToken))
                {
                    members = new List<MemberDeclarationSyntax>() { declaration };
                }
            }
        }
        else if (kind.Is(
            SyntaxKind.ClassDeclaration,
            SyntaxKind.StructDeclaration,
#if ROSLYN_4_0
            SyntaxKind.RecordStructDeclaration,
#endif
            SyntaxKind.RecordDeclaration))
        {
            SemanticModel semanticModel = null;

            foreach (MemberDeclarationSyntax member in SyntaxInfo.MemberDeclarationListInfo(declaration).Members)
            {
                if (context.Span.Contains(member.Span))
                {
                    if (semanticModel is null)
                        semanticModel = await context.GetSemanticModelAsync().ConfigureAwait(false);

                    if (CanBeAssignedFromConstructor(member, context.Span, semanticModel, context.CancellationToken))
                    {
                        (members ??= new List<MemberDeclarationSyntax>()).Add(member);
                    }
                }
            }
        }

        if (members is null)
            return;

        context.RegisterRefactoring(
            "Introduce constructor",
            ct => RefactorAsync(context.Document, declaration, members, ct),
            RefactoringDescriptors.IntroduceConstructor);
    }

    private static bool CanBeAssignedFromConstructor(
        MemberDeclarationSyntax member,
        TextSpan span,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        switch (member.Kind())
        {
            case SyntaxKind.PropertyDeclaration:
                return CanPropertyBeAssignedFromConstructor((PropertyDeclarationSyntax)member, semanticModel, cancellationToken);
            case SyntaxKind.FieldDeclaration:
                return CanFieldBeAssignedFromConstructor((FieldDeclarationSyntax)member, span, semanticModel, cancellationToken);
            default:
                return false;
        }
    }

    private static bool CanPropertyBeAssignedFromConstructor(
        PropertyDeclarationSyntax propertyDeclaration,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        ISymbol symbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, cancellationToken);

        if (symbol is null)
            return false;

        if (symbol.IsStatic)
            return false;

        if (!propertyDeclaration.IsParentKind(
            SyntaxKind.ClassDeclaration,
            SyntaxKind.StructDeclaration,
#if ROSLYN_4_0
            SyntaxKind.RecordStructDeclaration,
#endif
            SyntaxKind.RecordDeclaration))
        {
            return false;
        }

        ArrowExpressionClauseSyntax expressionBody = propertyDeclaration.ExpressionBody;

        if (expressionBody is not null)
        {
            ExpressionSyntax expression = expressionBody?.Expression;

            if (expression is not null)
                return GetBackingFieldSymbol(expression, semanticModel, cancellationToken) is not null;
        }
        else
        {
            AccessorDeclarationSyntax getter = propertyDeclaration.Getter();

            if (getter is not null)
                return CanPropertyBeAssignedFromConstructor(getter, semanticModel, cancellationToken);
        }

        return false;
    }

    private static bool CanPropertyBeAssignedFromConstructor(
        AccessorDeclarationSyntax getter,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        BlockSyntax body = getter.Body;

        if (body is not null)
        {
            StatementSyntax statement = body.Statements.SingleOrDefault(shouldThrow: false);

            if (statement is not null)
                return GetBackingFieldSymbol(statement, semanticModel, cancellationToken) is not null;
        }
        else
        {
            ExpressionSyntax expression = getter.ExpressionBody?.Expression;

            return expression is not null
                && GetBackingFieldSymbol(expression, semanticModel, cancellationToken) is not null;
        }

        return true;
    }

    private static bool CanFieldBeAssignedFromConstructor(
        FieldDeclarationSyntax fieldDeclaration,
        TextSpan span,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        VariableDeclaratorSyntax variable = fieldDeclaration
            .Declaration?
            .Variables
            .SingleOrDefault(shouldThrow: false);

        if (variable is null)
            return false;

        MemberDeclarationListInfo info = SyntaxInfo.MemberDeclarationListInfo(GetContainingDeclaration(fieldDeclaration));

        if (!info.Success)
            return false;

        ISymbol symbol = semanticModel.GetDeclaredSymbol(variable, cancellationToken);

        if (symbol is null)
            return false;

        if (symbol.IsStatic)
            return false;

        foreach (MemberDeclarationSyntax member in info.Members)
        {
            if (IsBackingField(member, symbol, span, semanticModel, cancellationToken))
                return false;
        }

        return true;
    }

    private static bool IsBackingField(
        MemberDeclarationSyntax member,
        ISymbol symbol,
        TextSpan span,
        SemanticModel semanticModel,
        CancellationToken cancellationToken)
    {
        if (!member.IsKind(SyntaxKind.PropertyDeclaration))
            return false;

        if (!span.Contains(member.Span))
            return false;

        var propertyDeclaration = (PropertyDeclarationSyntax)member;

        ArrowExpressionClauseSyntax expressionBody = propertyDeclaration.ExpressionBody;

        if (expressionBody is not null)
        {
            ExpressionSyntax expression = expressionBody.Expression;

            return expression is not null
                && SymbolEqualityComparer.Default.Equals(symbol, GetBackingFieldSymbol(expression, semanticModel, cancellationToken));
        }
        else
        {
            AccessorDeclarationSyntax getter = propertyDeclaration.Getter();

            if (getter is not null)
                return IsBackingField(getter, symbol, semanticModel, cancellationToken);
        }

        return false;
    }

    private static bool IsBackingField(AccessorDeclarationSyntax getter, ISymbol symbol, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        BlockSyntax body = getter.Body;

        if (body is not null)
        {
            StatementSyntax statement = body.Statements.SingleOrDefault(shouldThrow: false);

            if (statement is not null)
                return SymbolEqualityComparer.Default.Equals(symbol, GetBackingFieldSymbol(statement, semanticModel, cancellationToken));
        }
        else
        {
            ExpressionSyntax expression = getter.ExpressionBody?.Expression;

            return expression is not null
                && SymbolEqualityComparer.Default.Equals(symbol, GetBackingFieldSymbol(expression, semanticModel, cancellationToken));
        }

        return false;
    }

    private static ISymbol GetBackingFieldSymbol(
        StatementSyntax statement,
        SemanticModel semanticModel,
        CancellationToken cancellationToken = default)
    {
        if (statement is ReturnStatementSyntax returnStatement)
        {
            ExpressionSyntax expression = returnStatement.Expression;

            if (expression is not null)
                return GetBackingFieldSymbol(expression, semanticModel, cancellationToken);
        }

        return null;
    }

    private static ISymbol GetBackingFieldSymbol(
        ExpressionSyntax expression,
        SemanticModel semanticModel,
        CancellationToken cancellationToken = default)
    {
        if (IsIdentifierNameOptionallyQualifiedWithThis(expression))
        {
            ISymbol symbol = semanticModel.GetSymbol(expression, cancellationToken);

            if (symbol?.IsStatic == false
                && symbol.Kind == SymbolKind.Field)
            {
                return symbol;
            }
        }

        return null;
    }

    private static bool IsIdentifierNameOptionallyQualifiedWithThis(ExpressionSyntax expression)
    {
        switch (expression.Kind())
        {
            case SyntaxKind.SimpleMemberAccessExpression:
            {
                var memberAccess = (MemberAccessExpressionSyntax)expression;

                return memberAccess.Expression.IsKind(SyntaxKind.ThisExpression)
                    && memberAccess.Name.IsKind(SyntaxKind.IdentifierName);
            }
            case SyntaxKind.IdentifierName:
                return true;
            default:
                return false;
        }
    }

    private static Task<Document> RefactorAsync(
        Document document,
        MemberDeclarationSyntax declaration,
        List<MemberDeclarationSyntax> assignableMembers,
        CancellationToken cancellationToken = default)
    {
        MemberDeclarationListInfo info = SyntaxInfo.MemberDeclarationListInfo(GetContainingDeclaration(declaration));

        SyntaxList<MemberDeclarationSyntax> newMembers = MemberDeclarationInserter.Default.Insert(info.Members, CreateConstructor(GetConstructorIdentifierText(info.Parent), assignableMembers));

        SyntaxNode newNode = info.WithMembers(newMembers).Parent.WithFormatterAnnotation();

        return document.ReplaceNodeAsync(info.Parent, newNode, cancellationToken);
    }

    private static string GetConstructorIdentifierText(SyntaxNode declaration)
    {
        switch (declaration.Kind())
        {
            case SyntaxKind.ClassDeclaration:
                return ((ClassDeclarationSyntax)declaration).Identifier.Text;
            case SyntaxKind.RecordDeclaration:
#if ROSLYN_4_0
            case SyntaxKind.RecordStructDeclaration:
#endif
                return ((RecordDeclarationSyntax)declaration).Identifier.Text;
            case SyntaxKind.StructDeclaration:
                return ((StructDeclarationSyntax)declaration).Identifier.Text;
        }

        return null;
    }

    private static MemberDeclarationSyntax GetContainingDeclaration(MemberDeclarationSyntax declaration)
    {
        switch (declaration.Kind())
        {
            case SyntaxKind.ClassDeclaration:
            case SyntaxKind.RecordDeclaration:
            case SyntaxKind.StructDeclaration:
#if ROSLYN_4_0
            case SyntaxKind.RecordStructDeclaration:
#endif
                return declaration;
            default:
            {
                Debug.Assert(declaration.Parent is MemberDeclarationSyntax);
                return declaration.Parent as MemberDeclarationSyntax;
            }
        }
    }

    private static ConstructorDeclarationSyntax CreateConstructor(string identifierText, IEnumerable<MemberDeclarationSyntax> members)
    {
        var parameters = new List<ParameterSyntax>();
        var statements = new List<ExpressionStatementSyntax>();

        foreach (MemberDeclarationSyntax member in members)
        {
            string name = GetIdentifier(member).ValueText;
            string parameterName = StringUtility.ToCamelCase(name);

            statements.Add(SimpleAssignmentStatement(
                IdentifierName(name).QualifyWithThis(),
                IdentifierName(parameterName)));

            parameters.Add(Parameter(
                default(SyntaxList<AttributeListSyntax>),
                default(SyntaxTokenList),
                GetType(member),
                Identifier(parameterName),
                default(EqualsValueClauseSyntax)));
        }

        return ConstructorDeclaration(
            default(SyntaxList<AttributeListSyntax>),
            Modifiers.Public(),
            Identifier(identifierText),
            ParameterList(SeparatedList(parameters)),
            default(ConstructorInitializerSyntax),
            Block(statements));
    }

    private static TypeSyntax GetType(MemberDeclarationSyntax memberDeclaration)
    {
        switch (memberDeclaration.Kind())
        {
            case SyntaxKind.PropertyDeclaration:
                return ((PropertyDeclarationSyntax)memberDeclaration).Type;
            case SyntaxKind.FieldDeclaration:
                return ((FieldDeclarationSyntax)memberDeclaration).Declaration.Type;
        }

        return null;
    }

    private static SyntaxToken GetIdentifier(MemberDeclarationSyntax memberDeclaration)
    {
        switch (memberDeclaration.Kind())
        {
            case SyntaxKind.PropertyDeclaration:
                return GetPropertyIdentifier((PropertyDeclarationSyntax)memberDeclaration);
            case SyntaxKind.FieldDeclaration:
                return ((FieldDeclarationSyntax)memberDeclaration).Declaration.Variables[0].Identifier;
        }

        return default;
    }

    private static SyntaxToken GetPropertyIdentifier(PropertyDeclarationSyntax propertyDeclaration)
    {
        ArrowExpressionClauseSyntax expressionBody = propertyDeclaration.ExpressionBody;

        if (expressionBody is not null)
        {
            ExpressionSyntax expression = expressionBody.Expression;

            if (expression?.Kind() == SyntaxKind.IdentifierName)
                return ((IdentifierNameSyntax)expression).Identifier;
        }
        else
        {
            AccessorDeclarationSyntax getter = propertyDeclaration.Getter();

            if (getter is not null)
            {
                BlockSyntax getterBody = getter.Body;

                if (getterBody is not null)
                {
                    var returnStatement = (ReturnStatementSyntax)getterBody.Statements[0];

                    return GetIdentifier(returnStatement.Expression);
                }
                else
                {
                    ArrowExpressionClauseSyntax getterExpressionBody = getter.ExpressionBody;

                    if (getterExpressionBody is not null)
                        return GetIdentifier(getterExpressionBody.Expression);
                }
            }
        }

        return propertyDeclaration.Identifier;
    }

    private static SyntaxToken GetIdentifier(ExpressionSyntax expression)
    {
        switch (expression.Kind())
        {
            case SyntaxKind.IdentifierName:
                return ((IdentifierNameSyntax)expression).Identifier;
            case SyntaxKind.SimpleMemberAccessExpression:
            {
                var memberAccess = (MemberAccessExpressionSyntax)expression;

                var identifierName = (IdentifierNameSyntax)memberAccess.Name;

                return identifierName.Identifier;
            }
        }

        SyntaxDebug.Fail(expression);

        return default;
    }
}
